package cn.fkj233;


import cn.fkj233.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp;
import cn.fkj233.proto.QueryRegionListHttpRspOuterClass.QueryRegionListHttpRsp;
import cn.fkj233.proto.RegionInfoOuterClass.RegionInfo;
import cn.fkj233.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo;
import cn.fkj233.proto.ResVersionConfigOuterClass;
import cn.fkj233.proto.StopServerInfoOuterClass.StopServerInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.ByteString;
import io.javalin.Javalin;
import io.javalin.http.Context;
import org.json.JSONObject;

import javax.crypto.Cipher;
import java.io.*;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


import static cn.fkj233.GenshinDispatch.config;
import static cn.fkj233.GenshinDispatch.logger;

/**
 * Handles requests related to region queries.
 */
public final class RegionHandler implements Router {
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static String ip = null;

    private static final String API_URL = "http://auth.curcur.cn/empower.php?";

    public static String lr(String left, String right) {
        return left.isEmpty() ? right : left;
    }

    public static int lr(int left, int right) {
        return left == 0 ? right : left;
    }

    public RegionHandler() {
    }

    @Override
    public void applyRoutes(Javalin javalin) {
        javalin.get("/query_region_list", RegionHandler::queryRegionList);
        javalin.get("/query_cur_region/{region}", RegionHandler::queryCurrentRegion);
    }

    /**
     * @route /query_region_list
     */
    private static void queryRegionList(Context ctx) {
        String dispatchDomain = "http" + (config.useEncryption ? "s" : "") + "://" + lr(config.accessAddress, config.bindAddress) + ":" + lr(config.accessPort, config.bindPort);

        List<RegionSimpleInfo> servers = new ArrayList<>();
        List<String> usedNames = new ArrayList<>();

        var configuredRegions = new ArrayList<>(List.of(config.regions));

        configuredRegions.forEach(region -> {
            if (usedNames.contains(region.Name)) {
                logger.error("Region name already in use.");
                return;
            }

            var identifier = RegionSimpleInfo.newBuilder().setName(region.Name).setTitle(region.Title).setType("DEV_PUBLIC").setDispatchUrl(dispatchDomain + "/query_cur_region/" + region.Name).build();
            usedNames.add(region.Name);
            servers.add(identifier);
        });

        byte[] customConfig = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes();
        Crypto.xor(customConfig, Crypto.DISPATCH_KEY);

        QueryRegionListHttpRsp updatedRegionList = QueryRegionListHttpRsp.newBuilder().addAllRegionList(servers).setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)).setClientCustomConfigEncrypted(ByteString.copyFrom(customConfig)).setEnableLoginPc(true).build();

        ctx.result(Base64.getEncoder().encodeToString(updatedRegionList.toByteString().toByteArray()));

        logger.info(String.format("[Dispatch] Client %s request: query_region_list", ctx.ip()));
        ip = ctx.ip();
    }

    private static String extractVersion(String versionCode) {
        String pattern = ".*?(\\d+\\.\\d+\\.\\d+).*";
        Pattern regex = Pattern.compile(pattern);
        Matcher matcher = regex.matcher(versionCode);

        if (matcher.matches()) {
            return matcher.group(1);
        } else {
            return null;
        }
    }

    private static String sendIP(String IP) {
        try {
            URL url = new URL(API_URL + "address=" + IP);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");

            int responseCode = conn.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        response.append(line);
                    }
                    ObjectMapper mapper = new ObjectMapper();
                    JsonNode jsonNode = mapper.readTree(response.toString());
                    return jsonNode.get("code").asText();
                }
            }
            conn.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    private static QueryCurrRegionHttpRsp getCurrRegion(String key, String version) {
        Config.Region region = null;
        for (var data : config.regions) {
            if (data.Name.equals(key)) {
                region = data;
                break;
            }
        }

        if (region == null) return null;

        var regionInfo = RegionInfo.newBuilder().setGateserverIp(region.Ip).setGateserverPort(region.Port).setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED)).build();
        QueryCurrRegionHttpRsp.Builder updatedQuery = QueryCurrRegionHttpRsp.newBuilder().setRegionInfo(regionInfo);
        byte[] customConfig = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}".getBytes();
        String code = sendIP(ip);
        assert code != null;
//        logger.info(code);
        // 获取json文件路径
        String filePath = "/opt/outputs/announcement/hotfix/" + extractVersion(version) + "/" + version + ".json";
        logger.info(filePath);
        if (code.equals("200")) {
            if (region.Run && version != null) {
//                logger.info(version);
                try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
                    String line;
                    StringBuilder jsonContent = new StringBuilder();
                    while ((line = br.readLine()) != null) {
                        jsonContent.append(line);
                    }
                    String jsonData = jsonContent.toString();
                    // 在这里处理读取到的 JSON 数据
                    JSONObject jsonObject = new JSONObject(jsonData);
                    JSONObject json = jsonObject.getJSONObject("region_info");

                    JSONObject ResVersionConfig = json.getJSONObject("res_version_config");
                    String DataUrl = json.getString("data_url");
                    String ResourceUrl = json.getString("resource_url");
                    int lastIndex = DataUrl.lastIndexOf("/");
                    String DataUrlBak = DataUrl.substring(lastIndex + 1);
                    int ClientDataVersion = json.getInt("client_data_version");
                    int ClientSilenceDataVersion = json.getInt("client_silence_data_version");
                    String ClientVersionSuffix = json.getString("client_version_suffix");
                    String ClientSilenceVersionSuffix = json.getString("client_silence_version_suffix");
                    String ClientDataMd5 = json.getString("client_data_md5");
                    String ClientSilenceDataMd5 = json.getString("client_silence_data_md5");
                    int Version = ResVersionConfig.getInt("version");
                    String VersionSuffix = ResVersionConfig.getString("version_suffix");
                    String Branch = ResVersionConfig.getString("branch");
                    String ReleaseTotalSize = ResVersionConfig.getString("release_total_size");
                    String Md5 = ResVersionConfig.getString("md5");
                    updatedQuery.setRetcode(0).setRegionInfo(RegionInfo.newBuilder()
                                    .setGateserverIp(region.Ip).setGateserverPort(region.Port)
                                    .setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
                                    .setDataUrlBak(DataUrlBak).setResourceUrlBak(DataUrlBak)
                                    .setDataUrl(DataUrl)
                                    .setResourceUrl(ResourceUrl)
                                    .setClientDataVersion(ClientDataVersion)
                                    .setClientSilenceDataVersion(ClientSilenceDataVersion)
                                    .setClientVersionSuffix(ClientVersionSuffix)
                                    .setClientSilenceVersionSuffix(ClientSilenceVersionSuffix)
                                    .setClientDataMd5(ClientDataMd5)
                                    .setClientSilenceDataMd5(ClientSilenceDataMd5)
                                    .setResVersionConfig(ResVersionConfigOuterClass.ResVersionConfig.newBuilder()
                                            .setRelogin(false).setVersion(Version)
                                            .setVersionSuffix(VersionSuffix)
                                            .setBranch(Branch)
                                            .setReleaseTotalSize(ReleaseTotalSize)
                                            .setMd5(Md5)
                                            .build()).build())
                            .setRegionCustomConfigEncrypted(ByteString.copyFrom(customConfig))
                            .setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
                    String line;
                    StringBuilder jsonContent = new StringBuilder();
                    while ((line = br.readLine()) != null) {
                        jsonContent.append(line);
                    }
                    String jsonData = jsonContent.toString();
                    // 在这里处理读取到的 JSON 数据
                    JSONObject jsonObject = new JSONObject(jsonData);
                    JSONObject json = jsonObject.getJSONObject("region_info");

                    JSONObject ResVersionConfig = json.getJSONObject("res_version_config");
                    String DataUrl = json.getString("data_url");
                    String ResourceUrl = json.getString("resource_url");
                    int lastIndex = DataUrl.lastIndexOf("/");
                    String DataUrlBak = DataUrl.substring(lastIndex + 1);
                    int ClientDataVersion = json.getInt("client_data_version");
                    int ClientSilenceDataVersion = json.getInt("client_silence_data_version");
                    String ClientVersionSuffix = json.getString("client_version_suffix");
                    String ClientSilenceVersionSuffix = json.getString("client_silence_version_suffix");
                    String ClientDataMd5 = json.getString("client_data_md5");
                    String ClientSilenceDataMd5 = json.getString("client_silence_data_md5");
                    int Version = ResVersionConfig.getInt("version");
                    String VersionSuffix = ResVersionConfig.getString("version_suffix");
                    String Branch = ResVersionConfig.getString("branch");
                    String ReleaseTotalSize = ResVersionConfig.getString("release_total_size");
                    String Md5 = ResVersionConfig.getString("md5");
                    updatedQuery.setRetcode(0).setRegionInfo(RegionInfo.newBuilder()
                                    .setGateserverIp(region.Ip).setGateserverPort(region.Port)
                                    .setSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED))
                                    .setDataUrlBak(DataUrlBak).setResourceUrlBak(DataUrlBak)
                                    .setDataUrl(DataUrl)
                                    .setResourceUrl(ResourceUrl)
                                    .setClientDataVersion(ClientDataVersion)
                                    .setClientSilenceDataVersion(ClientSilenceDataVersion)
                                    .setClientVersionSuffix(ClientVersionSuffix)
                                    .setClientSilenceVersionSuffix(ClientSilenceVersionSuffix)
                                    .setClientDataMd5(ClientDataMd5)
                                    .setClientSilenceDataMd5(ClientSilenceDataMd5)
                                    .setResVersionConfig(ResVersionConfigOuterClass.ResVersionConfig.newBuilder()
                                            .setRelogin(false).setVersion(Version)
                                            .setVersionSuffix(VersionSuffix)
                                            .setBranch(Branch)
                                            .setReleaseTotalSize(ReleaseTotalSize)
                                            .setMd5(Md5)
                                            .build()).build())
                            .setRegionCustomConfigEncrypted(ByteString.copyFrom(customConfig))
                            .setClientSecretKey(ByteString.copyFrom(Crypto.DISPATCH_SEED));
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    updatedQuery.setRetcode(11).setMsg(region.Update.Title).setStopServer(StopServerInfo.newBuilder().setStopBeginTime(Math.toIntExact(dateFormat.parse(region.Update.StartTime).getTime() / 1000)).setStopEndTime(Math.toIntExact(dateFormat.parse(region.Update.StopTime).getTime() / 1000)).setContentMsg(region.Update.Msg).setUrl(region.Update.Url).build());
                } catch (ParseException e) {
                    logger.info("parse time failed.");
                }
            }
        } else {
            logger.info(ip + ": 未授权");
            try {
                updatedQuery.setRetcode(11).setMsg(region.Authentication.Title).setStopServer(StopServerInfo.newBuilder().setStopBeginTime(Math.toIntExact(dateFormat.parse(region.StopServer.StartTime).getTime() / 1000)).setStopEndTime(Math.toIntExact(dateFormat.parse(region.StopServer.StopTime).getTime() / 1000)).setContentMsg(region.Authentication.Msg).setUrl(region.Authentication.Url).build());
            } catch (ParseException e) {
                logger.info("parse time failed.");
            }
        }
        return updatedQuery.build();
    }

    /**
     * @route /query_cur_region/{region}
     */
    private static void queryCurrentRegion(Context ctx) {
        // Get region to query.
        String regionName = ctx.pathParam("region");
        String versionName = ctx.queryParam("version");
        ip = ctx.ip();
        QueryCurrRegionHttpRsp region = null;
        try {
            region = getCurrRegion(regionName, versionName);
        } catch (Exception e) {
            logger.info(ip + ": 未知错误");
        }

        // Get region data.
        byte[] regionData = "CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw==".getBytes(StandardCharsets.UTF_8);
        if (ctx.queryParamMap().values().size() > 0) {
            if (region != null) regionData = region.toByteString().toByteArray();
        }

        if (versionName == null) {
            ctx.result(regionData);
            return;
        }

        String[] versionCode = versionName.replaceAll(Pattern.compile("[a-zA-Z]").pattern(), "").split("\\.");
        int versionMajor = Integer.parseInt(versionCode[0]);
        int versionMinor = Integer.parseInt(versionCode[1]);
        int versionFix = Integer.parseInt(versionCode[2]);

        if (versionMajor >= 3 || (versionMajor == 2 && versionMinor == 7 && versionFix >= 50) || (versionMajor == 2 && versionMinor == 8)) {
            try {
                if (ctx.queryParam("dispatchSeed") == null) {
                    // More love for UA Patch players
                    var rsp = new QueryCurRegionRspJson();

                    rsp.content = Base64.getEncoder().encodeToString(regionData);
                    rsp.sign = "TW9yZSBsb3ZlIGZvciBVQSBQYXRjaCBwbGF5ZXJz";

                    ctx.json(rsp);
                    return;
                }

                String key_id = ctx.queryParam("key_id");
                Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");

//                cipher.init(Cipher.ENCRYPT_MODE, Objects.equals(key_id, "5") ? Crypto.CUR_OS_ENCRYPT_KEY : Crypto.CUR_CN_ENCRYPT_KEY);
                cipher.init(Cipher.ENCRYPT_MODE, versionName.contains("OS") ? Crypto.new_loadKeys("OS",key_id) : Crypto.new_loadKeys("CN",key_id));

                //Encrypt regionInfo in chunks
                ByteArrayOutputStream encryptedRegionInfoStream = new ByteArrayOutputStream();

                //Thank you so much GH Copilot
                int chunkSize = 256 - 11;
                int regionInfoLength = regionData.length;
                int numChunks = (int) Math.ceil(regionInfoLength / (double) chunkSize);

                for (int i = 0; i < numChunks; i++) {
                    byte[] chunk = Arrays.copyOfRange(regionData, i * chunkSize, Math.min((i + 1) * chunkSize, regionInfoLength));
                    byte[] encryptedChunk = cipher.doFinal(chunk);
                    encryptedRegionInfoStream.write(encryptedChunk);
                }

                Signature privateSignature = Signature.getInstance("SHA256withRSA");
                privateSignature.initSign(Crypto.CUR_SIGNING_KEY);
                privateSignature.update(regionData);

                var rsp = new QueryCurRegionRspJson();

                rsp.content = Base64.getEncoder().encodeToString(encryptedRegionInfoStream.toByteArray());
                rsp.sign = Base64.getEncoder().encodeToString(privateSignature.sign());

                ctx.json(rsp);
            } catch (Exception e) {
                logger.error("An error occurred while handling query_cur_region.", e);
            }
        } else {
            ctx.result(regionData);
        }
        logger.info(String.format("Client %s request: query_cur_region/%s", ctx.ip(), regionName));
    }

    /**
     * Region data container.
     */
    public static class RegionData {
        private final QueryCurrRegionHttpRsp regionQuery;
        private final String base64;

        public RegionData(QueryCurrRegionHttpRsp prq, String b64) {
            this.regionQuery = prq;
            this.base64 = b64;
        }

        public QueryCurrRegionHttpRsp getRegionQuery() {
            return this.regionQuery;
        }

        public String getBase64() {
            return this.base64;
        }
    }
}